/*******************************************************************************
 * Copyright 2013 Mindaugas Idzelis
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package pure.teamspeak;

import static com.google.common.collect.Lists.newArrayList;
import static com.google.common.collect.Maps.newConcurrentMap;
import incontrol.bf3.protocol.events.LevelLoadedEvent;
import incontrol.bf3.protocol.events.PlayerJoinEvent;
import incontrol.bf3.protocol.events.PlayerLeaveEvent;
import incontrol.bf3.protocol.events.PlayerSquadChangeEvent;
import incontrol.bf3.protocol.events.TeamScoresEvent;
import incontrol.bf3.protocol.events.datatype.Squad;
import incontrol.bf3.protocol.requests.MovePlayerRequest;
import incontrol.bf3.state.PlayerList;
import incontrol.bf3.state.PlayerState;
import incontrol.core.Config;
import incontrol.core.group.Group;
import incontrol.core.group.GroupRequest;
import incontrol.core.group.SinglePlayerGroup;
import incontrol.core.protocol.events.StatsRefreshedEvent;
import incontrol.core.protocol.responses.GenericResponse;
import incontrol.plugin.Plugin;
import incontrol.plugin.protocol.INSubscribe;

import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Vector;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import pure.teamspeak.client.JTS3ServerQuery;
import pure.teamspeak.client.TeamspeakActionListener;

import com.google.common.base.Function;
import com.google.common.base.Joiner;
import com.google.common.base.Predicate;
import com.google.common.base.Splitter;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import com.google.common.collect.Ordering;
import com.google.common.net.HostAndPort;
import com.google.common.util.concurrent.Uninterruptibles;

public class PureTeamSpeak extends Plugin implements TeamspeakActionListener {

	private Map<Integer, TSClient> teamSpeakUsers;
	private BiMap<Squad, Integer> squadChannels = Maps
			.synchronizedBiMap(HashBiMap.<Squad, Integer> create());
	private BiMap<Integer, Integer> teamChannels = Maps
			.synchronizedBiMap(HashBiMap.<Integer, Integer> create());
	protected JTS3ServerQuery query;
	private int rootId;
	private ScheduledExecutorService scheduler = Executors
			.newScheduledThreadPool(2);
	private boolean betweenRounds;
	private boolean kickToMakeRoom;
	private List<String> disabledAutoSquad = newArrayList();

	private List<Integer> getClients(List<Integer> channels) {
		Vector<HashMap<String, String>> list = query
				.getList(JTS3ServerQuery.LISTMODE_CLIENTLIST);
		List<Integer> clients = newArrayList();
		for (HashMap<String, String> hashMap : list) {
			int channelId = Integer.parseInt(hashMap.get("cid"));
			if (channels.contains(channelId)) {
				clients.add(Integer.parseInt(hashMap.get("clid")));
			}
		}
		return clients;
	}

	private List<Integer> getChildren(Integer channel) {
		Vector<HashMap<String, String>> channels = query.getList(
				JTS3ServerQuery.LISTMODE_CHANNELLIST, null);
		List<Integer> children = newArrayList();
		for (HashMap<String, String> hashMap : channels) {
			if (channel.equals(Integer.parseInt(hashMap.get("pid")))) {
				children.add(Integer.parseInt(hashMap.get("cid")));
			}
		}
		return children;
	}

	public void init(Config config) {

		String hostString = config.get("TeamSpeak Host");
		String userString = config.get("TeamSpeak Username");
		String passString = config.get("TeamSpeak Password");

		query = new JTS3ServerQuery();
		HostAndPort host = HostAndPort.fromString(hostString);
		boolean connected = query.connectTS3Query(host.getHostText(),
				host.getPort());
		if (connected) {
			query.loginTS3(userString, passString);

			Vector<HashMap<String, String>> servers = query.getList(
					JTS3ServerQuery.LISTMODE_SERVERLIST, null);
			String id = servers.get(0).get("virtualserver_id");
			query.selectVirtualServer(Integer.parseInt(id));
			query.doCommand("clientupdate client_nickname=PureTeamSync");

			teamSpeakUsers = newConcurrentMap();
			String rootChannel = "BF3";

			rootId = ensureChannel(rootChannel, -1);

			// don't dellete channels on startup, instead, recover/reclaim them
			List<Integer> allChildren = newArrayList();
			List<Integer> queue = newArrayList(rootId);
			while (!queue.isEmpty()) {
				Integer channel = queue.remove(0);
				List<Integer> children = getChildren(channel);
				queue.addAll(children);
				allChildren.addAll(children);
			}

			List<Integer> clients = getClients(allChildren);
			for (Integer integer : clients) {
				query.moveClient(integer, rootId, "");
			}
			for (Integer integer : allChildren) {
				query.deleteChannel(integer, true);
			}

			query.setTeamspeakActionListener(this);
			query.addEventNotify(JTS3ServerQuery.EVENT_MODE_SERVER, -1);
			query.addEventNotify(JTS3ServerQuery.EVENT_MODE_CHANNEL, rootId);
			getEventBus().register(this);

			final CountDownLatch init = new CountDownLatch(1);
			scheduler.scheduleAtFixedRate(new Runnable() {

				@Override
				public void run() {
					updateTSClientState();
					init.countDown();
				}

			}, 0, 1000, TimeUnit.MILLISECONDS);
			Uninterruptibles.awaitUninterruptibly(init);

			betweenRounds = getRound().teamScores.scores.size() == 0;
			handleStatsRefreshedEvent(null);
		}
	}

	public synchronized void updateTSClientState() {
		Vector<HashMap<String, String>> list = query
				.getList(JTS3ServerQuery.LISTMODE_CLIENTLIST);

		for (HashMap<String, String> hashMap : list) {
			int id = Integer.parseInt(hashMap.get("clid"));
			int channelId = Integer.parseInt(hashMap.get("cid"));
			String name = hashMap.get("client_nickname");
			TSClient client = teamSpeakUsers.get(id);
			if (client == null) {
				if (channelId == rootId
						|| teamChannels.containsValue(channelId)
						|| squadChannels.containsValue(channelId)) {
					client = new TSClient(id, name, channelId);
					teamSpeakUsers.put(id, client);
				}
			} else {
				if (channelId == rootId
						|| teamChannels.containsValue(channelId)
						|| squadChannels.containsValue(channelId)) {
					client.setChannelId(channelId);
				} else {
					teamSpeakUsers.remove(id);
				}
			}
		}
		for (Iterator<Integer> iterator = squadChannels.values().iterator(); iterator
				.hasNext();) {
			Integer squad = (Integer) iterator.next();
			int count = countInChannel(squad, list);
			if (count == 0) {
				query.deleteChannel(squad, true);
				iterator.remove();
			}
		}

	}

	private int countInChannel(int squad, Vector<HashMap<String, String>> list) {
		int clients = 0;
		for (HashMap<String, String> hashMap : list) {
			if (squad == Integer.parseInt(hashMap.get("cid"))) {
				clients++;
			}
		}
		return clients;
	}

	@Override
	public synchronized void teamspeakActionPerformed(String eventType,
			HashMap<String, String> eventInfo) {
		System.out.print(eventType);
		System.out.println(Joiner.on(',').withKeyValueSeparator("=")
				.join(eventInfo));
		if ("notifyclientleftview".equals(eventType)) {
			teamSpeakUsers.remove(Integer.parseInt(eventInfo.get("clid")));
		} else if ("notifyclientmoved".equals(eventType)) {
			int clid = Integer.parseInt(eventInfo.get("clid"));
			if (rootId == Integer.parseInt(eventInfo.get("ctid"))) {
				TSClient tsClient = teamSpeakUsers.get(clid);
				if (tsClient != null) {
					ensureBF3UserInChannel(tsClient.getNickname(), tsClient);
				}
			} else {
				teamSpeakUsers.remove(clid);
			}

		}
	}

	@INSubscribe
	public synchronized void handleStatsRefreshedEvent(StatsRefreshedEvent event) {
		if (!betweenRounds) {
			ensureTeamChannels();
		} else {
			moveAllToLobby();
		}
		for (PlayerState player : getPlayerList().getPlayers()) {
			ensurePlayerInChannel(player.name);
		}
	}

	@INSubscribe
	public synchronized void handlePlayerJoinEvent(PlayerJoinEvent event) {
		String playerName = event.getPlayerName();
		ensurePlayerInChannel(playerName);
	}

	private void ensurePlayerInChannel(String playerName) {

		ensureBF3UserInChannel(playerName, getTSUserByName(playerName));

	}

	private void ensureBF3UserInChannel(String playerName, TSClient client) {
		if (client == null)
			return;
		PlayerState playerState = getPlayerList().byName().get(playerName);
		if (playerState != null) {
			if (!betweenRounds) {
				ensureTeamChannels();

				if (playerState.squadId == 0) {
					Integer team = teamChannels.get(playerState.teamId);
					if (team == null) {
						moveClient(client, rootId);
					} else {
						moveClient(client, team);
					}
				} else {
					String squadName = Squad.getSquadName(playerState.squadId);
					int squadChannel = ensureSquadChannel(squadName, new Squad(
							playerState.teamId, playerState.squadId));
					if (client.getChannelId() != squadChannel) {
						moveClient(client, squadChannel);
					}
				}
			}
		} else {
			moveClient(client, rootId);
		}

	}

	private void moveClient(TSClient client, int channel) {
		boolean success = query.moveClient(client.getClientId(), channel, "");
		if (success) {
			client.setChannelId(channel);
		}
	}

	private int ensureSquadChannel(String squadName, Squad squad) {
		Integer integer = squadChannels.get(squad);
		if (integer == null) {
			int parent = teamChannels.get(squad.getTeamId());
			integer = ensureChannel(squadName, parent);
			squadChannels.forcePut(squad, integer);
		}
		return integer;
	}

	private void moveChannel(int id, int parent) {
		query.doCommand(String.format("channelmove cid=%s cpid=%s", id, parent));
	}

	private synchronized int ensureChannel(String name, int parent) {
		int id;
		Map<String, String> channel = getChannel(name);
		if (null == channel) {
			HashMap<String, String> doCommand = query.doCommand(String.format(
					"channelcreate channel_name=%s channel_flag_permanent=1",
					name));
			String response = doCommand.get("response");
			Map<String, String> split = Splitter.on(',')
					.withKeyValueSeparator('=').split(response);
			id = Integer.parseInt(split.get("cid"));
		} else {
			id = Integer.parseInt(channel.get("cid"));
		}
		if (parent >= 0) {
			moveChannel(id, parent);
		}
		return id;
	}

	Map<String, String> getChannel(String name) {
		Vector<HashMap<String, String>> channels = query.getList(
				JTS3ServerQuery.LISTMODE_CHANNELLIST, null);
		for (HashMap<String, String> hashMap : channels) {
			if (name.equals(hashMap.get("channel_name"))) {
				return hashMap;
			}
		}
		return null;
	}

	@INSubscribe
	public synchronized void handlePlayerSquadChangeEvent(
			PlayerSquadChangeEvent event) {
		moveToFreeTSSquad(event.getPlayerName(), event.getSquad(), false);
	}

	private synchronized void moveToFreeTSSquad(String playerName,
			final Squad squad, boolean teamEvent) {
		// if destination squad already has TS users, done.
		// scan the team to see if we can pair user with any other team-user
		// already present
		// if no match, no change
		// find squad with open slots... if no open slots, then find squad
		// with
		// non-TS members, kick one, and make room
		if (getTSUserByName(playerName) == null) {
			// player does not have TS
			return;
		}
		if (disabledAutoSquad.contains(playerName))
			return;

		if (squad.getSquadId() == 0 || squad.getTeamId() == 0)
			return;

		ensureBF3UserInChannel(playerName, getTSUserByName(playerName));

		Multimap<Squad, TSClient> clients = ArrayListMultimap.create();
		for (TSClient ts : teamSpeakUsers.values()) {
			Squad otherSquad = squadChannels.inverse().get(ts.getChannelId());
			if (otherSquad != null) {
				clients.put(otherSquad, ts);
			}
		}
		if (clients.get(squad).size() - 1 > 0) {
			// target squad already has people, excluding myself
			return;
		}
		clients.removeAll(squad);
		Multimap<Squad, TSClient> teamClients = Multimaps.filterKeys(clients,
				new Predicate<Squad>() {
					public boolean apply(Squad input) {
						return input.teamId == squad.teamId;
					};
				});
		List<Entry<Squad, TSClient>> entries = newArrayList(teamClients
				.entries());
		final Multimap<Squad, PlayerState> serverSquads = getPlayerList()
				.getSquads();
		Ordering<Entry<Squad, TSClient>> ordering = Ordering.natural()
				.onResultOf(new Function<Entry<Squad, TSClient>, Integer>() {
					public Integer apply(Entry<Squad, TSClient> input) {
						Collection<PlayerState> players = serverSquads
								.get(input.getKey());
						return players.size();
					};
				});
		Collections.sort(entries, ordering);
		for (Entry<Squad, TSClient> entry : entries) {
			Collection<PlayerState> c = serverSquads.get(entry.getKey());
			if (c.size() < 4) {
				getEventBus()
						.sendServerRequest(
								new MovePlayerRequest(playerName, entry
										.getKey(), false));
				return;
			}
		}
		// if not done
		if (kickToMakeRoom) {
			for (Entry<Squad, TSClient> entry : entries) {
				Collection<PlayerState> c = serverSquads.get(entry.getKey());
				if (c.size() < 4) {
					getEventBus().sendServerRequest(
							new MovePlayerRequest(playerName, entry.getKey(),
									false));
					return;
				} else {
					for (PlayerState playerState : c) {
						TSClient client = getTSUserByName(playerState.name);
						if (client == null) {
							// kick client
							getEventBus().sendServerRequest(
									new MovePlayerRequest(playerState.name,
											playerState.teamId, 0, false));
							getEventBus().sendServerRequest(
									new MovePlayerRequest(playerName, entry
											.getKey(), false));
							return;
						}
					}
				}
			}
		}
	}

	private synchronized TSClient getTSUserByName(String name) {
		for (TSClient client : teamSpeakUsers.values()) {
			if (name.equals(client.getNickname())) {
				return client;
			}
		}
		return null;
	}

	@INSubscribe
	public synchronized void handlePlayerLeaveEvent(PlayerLeaveEvent event) {
		String playerName = event.getPlayerName();
		ensurePlayerInChannel(playerName);
	}

	@INSubscribe
	public synchronized void handleLevelLoadedEvent(LevelLoadedEvent event) {
		betweenRounds = false;
		ensureTeamChannels();
	}

	private void ensureTeamChannels() {
		Integer usId = teamChannels.get(1);
		if (usId == null) {
			usId = ensureChannel("US", rootId);
			teamChannels.put(1, usId);
		}
		// depending on game mode
		Integer rusId = teamChannels.get(2);
		if (rusId == null) {
			rusId = ensureChannel("RU", rootId);
			teamChannels.put(2, rusId);
		}
	}

	@INSubscribe
	public synchronized void handleRoundOverEvent(TeamScoresEvent event) {
		betweenRounds = true;
		moveAllToLobby();

		disabledAutoSquad.clear();
		teamChannels.clear();
		squadChannels.clear();

	}

	private void moveAllToLobby() {
		for (TSClient client : teamSpeakUsers.values()) {
			moveClient(client, rootId);
		}
		for (Integer squadChannel : squadChannels.values()) {
			boolean success = query.deleteChannel(squadChannel, true);
			if (success) {
				squadChannels.remove(squadChannel);
			}
		}
		for (Integer squadChannel : teamChannels.values()) {
			boolean success = query.deleteChannel(squadChannel, true);
			if (success) {
				teamChannels.remove(squadChannel);
			}
		}
	}

	@INSubscribe
	public void handleGroupRequest(GroupRequest req) {
		List<Group> groups = newArrayList();
		PlayerList playerList = getPlayerList();
		for (PlayerState player : playerList.getPlayers()) {
			groups.add(new SinglePlayerGroup(player.name));
		}
		sendResponse(req, new GenericResponse(groups));
	}
}
